using System;
using System.Collections.Generic;
using System.IO;
using System.Diagnostics;
using MLSharp.Classification;
using MLSharp.RProject;
using MLSharp.Utilities;

namespace MLSharp.Clustering
{
	/// <summary>
	/// A Knn created via the KnnFlex package.  See <see cref="KnnFactory"/>.
	/// </summary>
	public class KnnClassifier : IClassifier
	{
		#region Logger

		/// <summary>
		/// The logger for the class.
		/// </summary>
		private static readonly log4net.ILog mLogger = log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);

		#endregion

		#region Private Fields

		/// <summary>
		/// Stores the training data
		/// </summary>
		private IDataSet train;

		/// <summary>
		/// The working directory to use.
		/// </summary>
		private readonly string mWorkingDirectory;

		/// <summary>
		/// The path to the R executable.
		/// </summary>
		private readonly string mRPath;

		/// <summary>
		/// The options for running KNN.
		/// </summary>
		private readonly KnnOptions mOptions;

		#endregion

		#region Const Fields

		/// <summary>
		/// The name of the NN library to use.
		/// </summary>
		private const string mLibraryName = "kknn";

		#endregion

		#region Public Constructors

		/// <summary>
		/// Creates a nearest neighbor.
		/// </summary>
		/// <param name="workingDirectory">The working directory to use.</param>
		/// <param name="rPath">The path to R.exe.</param>
		/// <param name="train"></param>
		/// <param name="options"></param>
		public KnnClassifier(string workingDirectory, string rPath, IDataSet train, KnnOptions options) 
		{
			mWorkingDirectory = workingDirectory;
			mRPath = rPath;
			this.train = train;
			mOptions = options;
		}

		#endregion

		#region Public Methods

		/// <summary>
		/// Classifies the specified data set.
		/// </summary>
		/// <param name="test"></param>
		/// <returns></returns>
		public virtual ClassificationResult[] Classify(IDataSet test)
		{
			//This is the file that the instances will be written to.
			string testFilename = Path.Combine(mWorkingDirectory, NameGenerator.GetRandomName(10) + ".table");

			//This is the file that the instances will be written to.
			string trainFilename = Path.Combine(mWorkingDirectory, NameGenerator.GetRandomName(10) + ".table");

			//And this is the script that is fed in to R.
			string rScriptFilename = Path.Combine(mWorkingDirectory, NameGenerator.GetRandomName(10) + ".R");

			//Step 1: Write the data set to a temp table file.
			RTableWriter.Write(test, testFilename);
			RTableWriter.Write(train, trainFilename);

			//Applying feature weghting
			for(int instance = 0; instance < test.Instances.Count; instance++)
			{
				for(int j = 1; j < test.Instances[instance].Values.Length; j++)
				{
					test.Instances[instance].Values[j] = Convert.ToDouble(test.Instances[instance].Values[j]) * mOptions.weight[j]; 
					//Convert.ToString(Convert.ToDouble(test.Instances[instance].Values[j]) * mOptions.weight[j]); 
				}
			}

			List<string> output = new List<string>();

			//Step 2: Create a new R script
			using (StreamWriter writer = File.CreateText(rScriptFilename))
			{
				//Step 3a: Import the neural network library
				writer.WriteLine("library({0})", mLibraryName);

				//Step 4: The command to load the data set from the temp file.
				writer.WriteLine("test<-read.table(\"{0}\", header = FALSE)", testFilename.Replace(@"\", @"\\"));
				writer.WriteLine("train<-read.table(\"{0}\", header = FALSE)", trainFilename.Replace(@"\", @"\\"));

				string predict = "pred<-kknn(formula = formula(train), train, test, k={0}, distance={1}, kernel=\"{2}\")";
				predict = string.Format(predict, mOptions.k, mOptions.distance, mOptions.kernel);

				if (mLogger.IsInfoEnabled)
				{
					//mLogger.InfoFormat("Training network: {0}", trainNetwork);
					mLogger.InfoFormat("Starting classifcation: {0}", predict);
				}
				//Step 5: The command to run the network
				writer.WriteLine("{0}", predict);

				//Step 6: The command to dump the results
				writer.WriteLine("rbind(test$NeedTC, test$uniqueid, fitted(pred))");
			}

			//Step 7: Run R and capture its output.
			ProcessStartInfo startInfo = new ProcessStartInfo(mRPath);
			startInfo.RedirectStandardOutput = true;
			startInfo.RedirectStandardError = true;
			startInfo.WorkingDirectory = mWorkingDirectory;
			startInfo.Arguments = string.Format("-f \"{0}\" --slave", rScriptFilename);
			//This must be false if you are redirecting input/output
			startInfo.UseShellExecute = false;
			startInfo.CreateNoWindow = true;
			output = new List<string>();
            

			using (Process rProcess = Process.Start(startInfo))
			{
				if (rProcess == null)
				{
					throw new NullReferenceException("rProcess is null!");
				}

				while (!rProcess.StandardOutput.EndOfStream)
				{
					output.Add(rProcess.StandardOutput.ReadLine());
				}
				rProcess.WaitForExit();

				//You can uncomment this line if you need to see the raw results.
				//Console.WriteLine(string.Join("\r\n", output.ToArray()));
			}

			//Remove temporary files
			File.Delete(trainFilename);
			File.Delete(testFilename);
			File.Delete(rScriptFilename);

			//Step 8: Parse the results
			output = ValidateAndPrepareOutput(test, output);

			//Allocate the result array
			ClassificationResult[] results = new ClassificationResult[output.Count];

			//Loop through the output and extract the results.
			for (int i = 0; i < output.Count; i++)
			{
				//Grab an ID for this instance, if any.
                
				//string id = test.HasLabelAttribute ? test.Instances[i].Values[test.LabelAttributeIndex] : "";
				string id = test.Instances[i].Label;

				double value = double.Parse(output[i]);

				//Find the best class for this instance.
				string classification = test.Attributes[test.TargetAttributeIndex].GetClosestValue(value);

				double confidence = test.Attributes[test.TargetAttributeIndex].GetConfidenceForClosestValue(value);

				results[i] = new ClassificationResult(classification, test.Instances[i].ClassValue)
				             	{Confidence = confidence, ID = id};
			}

			return results;
		}
		#endregion

		#region Protected Methods

		/// <summary>
		/// Verifies that the output is in the correct, expected format and
		/// performs any necessary cleaning.
		/// </summary>
		/// <param name="dataSet"></param>
		/// <param name="output"></param>
		/// <exception cref="InvalidOperationException">Thrown if there is a problem with the data.</exception>
		/// <returns>Returns the parsed output strings</returns>
		protected List<string> ValidateAndPrepareOutput(IDataSet dataSet, List<string> output)
		{
			List<string> tempOutput = new List<string>();
			for (int i = 1; i < output.Count; i+=2)
			{
				String temp = output[i];
				temp = temp.Substring(5, temp.Length-5);//cut off the first part of the line "[1, ] "
				while(temp.Length != 0)
				{
					if (temp.Contains(" "))
					{
						while(temp.StartsWith(" "))
						{
							temp = temp.Substring(temp.IndexOf(" ") + 1, temp.Length - (temp.IndexOf(" ") + 1));
						}
						if(temp.Contains(" "))
						{
							tempOutput.Add(temp.Substring(0, temp.IndexOf(" ")));
						}
						else
						{
							tempOutput.Add(temp);
							temp = "";
						}
						temp = temp.Substring(temp.IndexOf(" ") + 1, temp.Length - (temp.IndexOf(" ") + 1));
					}
					else
					{
						tempOutput.Add(temp);
						temp = "";
					}
				}
			}
			output = tempOutput;
			//Verify that the expected number of lines were written.
			if (output.Count != dataSet.Instances.Count)
			{
				throw new InvalidOperationException(
					string.Format("Expected {0} output lines, found {1}.", dataSet.Instances.Count, output.Count));
			}
			return output;
			//Verify that the results begin with the expected output.
			//Don't need to do this because KNN doesn't have a header coming back.
			/*
			if (!output[0].EndsWith("[,1]"))
			{
				throw new InvalidOperationException(
					string.Format("Output does not begin with expected header line, found '{0}'.", output[0]));
			}

			//Dump the header line, we don't need it.
			output.RemoveAt(0);
            */
		}
		#endregion
	}
}
